package prefuse.activity;

import java.util.ArrayList;

import prefuse.util.PrefuseConfig;

/**
 * <p>The ActivityManager is responsible for scheduling and running timed 
 * activities that perform data processing and animation.</p>
 * 
 * <p>The AcivityManager runs in its own separate thread of execution, and
 * one instance is used to schedule activities from any number of currently
 * active visualizations. The class is implemented as a singleton; the single
 * instance of this class is interacted with through static methods. These
 * methods are called by an Activity's run methods, and so are made only
 * package visible here.</p>
 * 
 * <p>Activity instances can be scheduled by using their  
 * {@link prefuse.activity.Activity#run()},
 * {@link prefuse.activity.Activity#runAt(long)}, and 
 * {@link prefuse.activity.Activity#runAfter(Activity)}
 * methods. These will automatically call the
 * appropriate methods with the ActivityManager.</p>
 * 
 * <p>For {@link prefuse.action.Action} instances, one can also register
 * the actions with a {@link prefuse.Visualization} and use the
 * visualizations provided run methods to launch Actions in a
 * convenient fashion. The interface, which is backed by an {@link ActivityMap}
 * instance, also provides a useful level of indirection, allowing actions
 * to be changed dynamically without changes to code in other locations.
 * </p>
 *
 * @author <a href="http://jheer.org">jeffrey heer</a>
 * @see Activity
 * @see prefuse.action.Action
 */
public class ActivityManager extends Thread {

    private static ActivityManager s_instance;

    private ArrayList m_activities;
    private ArrayList m_tmp;
    private long m_nextTime;
    private boolean m_run;

    /**
     * Returns the active ActivityManager instance.
     * @return the ActivityManager
     */
    private synchronized static ActivityManager getInstance() {
        if (s_instance == null || !s_instance.isAlive()) {
            s_instance = new ActivityManager();
        }
        return s_instance;
    }

    /**
     * Create a new ActivityManger.
     */
    private ActivityManager() {
        super("prefuse_ActivityManager");
        m_activities = new ArrayList();
        m_tmp = new ArrayList();
        m_nextTime = Long.MAX_VALUE;

        int priority = PrefuseConfig.getInt("activity.threadPriority");
        if (priority >= Thread.MIN_PRIORITY && priority <= Thread.MAX_PRIORITY) {
            this.setPriority(priority);
        }
        this.setDaemon(true);
        this.start();
    }

    /**
     * Stops the activity manager thread. All scheduled actvities are
     * canceled, and then the thread is then notified to stop running.
     */
    public static void stopThread() {
        ActivityManager am;
        synchronized (ActivityManager.class) {
            am = s_instance;
        }
        if (am != null)
            am._stop();
    }

    /**
     * Schedules an Activity with the manager.
     * @param a the Activity to schedule
     */
    static void schedule(Activity a) {
        getInstance()._schedule(a, a.getStartTime());
    }

    /**
     * Schedules an Activity to start immediately, overwriting the
     * Activity's currently set startTime.
     * @param a the Activity to schedule
     */
    static void scheduleNow(Activity a) {
        getInstance()._schedule(a, System.currentTimeMillis());
    }

    /**
     * Schedules an Activity at the specified startTime, overwriting the
     * Activity's currently set startTime.
     * @param a the Activity to schedule
     * @param startTime the time at which the activity should run
     */
    static void scheduleAt(Activity a, long startTime) {
        getInstance()._schedule(a, startTime);
    }

    /**
     * Schedules an Activity to start immediately after another Activity.
     * The second Activity will be scheduled to start immediately after the
     * first one finishes, overwriting any previously set startTime. If the
     * first Activity is cancelled, the second one will not run.
     * 
     * This functionality is provided by using an ActivityListener to monitor
     * the first Activity. The listener is removed upon completion or
     * cancellation of the first Activity.
     * 
     * This method does not effect the scheduling of the first Activity.
     * @param before the first Activity to run
     * @param after the Activity to run immediately after the first
     */
    static void scheduleAfter(Activity before, Activity after) {
        getInstance()._scheduleAfter(before, after);
    }

    /**
     * Schedules an Activity to start immediately after another Activity.
     * The second Activity will be scheduled to start immediately after the
     * first one finishes, overwriting any previously set startTime. If the
     * first Activity is cancelled, the second one will not run.
     * 
     * This functionality is provided by using an ActivityListener to monitor
     * the first Activity. The listener will persist across mulitple runs,
     * meaning the second Activity will always be evoked upon a successful
     * finish of the first.
     * 
     * This method does not otherwise effect the scheduling of the first Activity.
     * @param before the first Activity to run
     * @param after the Activity to run immediately after the first
     */
    static void alwaysScheduleAfter(Activity before, Activity after) {
        getInstance()._alwaysScheduleAfter(before, after);
    }

    /**
     * Cancels an Activity and removes it from this manager, called by
     * an Activity when the activity needs to be cancelled. 
     * @param a The activity to cancel.
     */
    static void cancelActivity(Activity a) {
        getInstance()._cancelActivity(a);
    }

    /**
     * Returns the number of scheduled activities
     * @return the number of scheduled activities
     */
    public static int activityCount() {
        return getInstance()._activityCount();
    }

    /**
     * Stops the activity manager thread. All scheduled actvities are
     * canceled, and then the thread is then notified to stop running.
     */
    private synchronized void _stop() {
        while (m_activities.size() > 0) {
            Activity a = (Activity) m_activities.get(m_activities.size() - 1);
            a.cancel();
        }
        _setRunning(false);
        notify();
    }

    /**
     * Schedules an Activity with the manager.
     * @param a the Activity to schedule
     */
    private void _schedule(Activity a, long startTime) {
        if (a.isScheduled()) {
            try {
                notifyAll();
            } catch (Exception e) {
            }
            return; // already scheduled, do nothing
        }
        a.setStartTime(startTime);
        synchronized (this) {
            m_activities.add(a);
            a.setScheduled(true);
            if (startTime < m_nextTime) {
                m_nextTime = startTime;
                notify();
            }
        }
    }

    /**
     * Schedules an Activity to start immediately after another Activity.
     * The second Activity will be scheduled to start immediately after the
     * first one finishes, overwriting any previously set startTime. If the
     * first Activity is cancelled, the second one will not run.
     * 
     * This functionality is provided by using an ActivityListener to monitor
     * the first Activity. The listener is removed upon completion or
     * cancellation of the first Activity.
     * 
     * This method does not effect the scheduling of the first Activity.
     * @param before the first Activity to run
     * @param after the Activity to run immediately after the first
     */
    private void _scheduleAfter(Activity before, Activity after) {
        before.addActivityListener(new ScheduleAfterActivity(after, true));
    }

    /**
     * Schedules an Activity to start immediately after another Activity.
     * The second Activity will be scheduled to start immediately after the
     * first one finishes, overwriting any previously set startTime. If the
     * first Activity is cancelled, the second one will not run.
     * 
     * This functionality is provided by using an ActivityListener to monitor
     * the first Activity. The listener will persist across mulitple runs,
     * meaning the second Activity will always be evoked upon a successful
     * finish of the first.
     * 
     * This method does not otherwise effect the scheduling of the first Activity.
     * @param before the first Activity to run
     * @param after the Activity to run immediately after the first
     */
    private void _alwaysScheduleAfter(Activity before, Activity after) {
        before.addActivityListener(new ScheduleAfterActivity(after, false));
    }

    /**
     * Cancels an action, called by an Activity when it is cancelled. 
     * Application code should not call this method! Instead, use 
     * Activity.cancel() to stop a sheduled or running Activity.
     * @param a The Activity to cancel
     */
    private void _cancelActivity(Activity a) {
        /*
         * Prefuse Bug ID #1708926
         * The fix ("Contribution") has not been tested and/or validated for release as or in products,
         * combinations with products or other commercial use.
         * Any use of the Contribution is entirely made at the user's own responsibility and the user can
         * not rely on any features, functionalities or performances Alcatel-Lucent has attributed to the Contribution.
         * THE CONTRIBUTION BY ALCATEL-LUCENT (...) IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND,
         * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
         * FITNESS FOR A PARTICULAR PURPOSE, COMPLIANCE, NON-INTERFERENCE  AND/OR INTERWORKING WITH THE SOFTWARE
         * TO WHICH THE CONTRIBUTION HAS BEEN MADE, TITLE AND NON-INFRINGEMENT.
         * IN NO EVENT SHALL ALCATEL-LUCENT (...) BE LIABLE FOR ANY DAMAGES OR OTHER LIABLITY,
         * WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE CONTRIBUTION
         * OR THE USE OR OTHER DEALINGS IN THE CONTRIBUTION,
         * WHETHER TOGETHER WITH THE SOFTWARE TO WHICH THE CONTRIBUTION RELATES OR ON A STAND ALONE BASIS.
         */
        boolean fire = false;
        //removeActivity synchronizes on this, we need to lock this
        //before we lock the activity to avoid deadlock
        synchronized (this) {
            synchronized (a) {
                if (a.isScheduled()) {
                    // attempt to remove this activity, if the remove fails,
                    // this activity is not currently scheduled with the manager
                    _removeActivity(a);
                    fire = true;
                }
                a.setRunning(false);
            }
        }
        if (fire)
            a.fireActivityCancelled();
    }

    /**
     * Removes an Activity from this manager, called by an
     * Activity when it finishes or is cancelled. Application 
     * code should not call this method! Instead, use 
     * Activity.cancel() to stop a sheduled or running Activity.
     * @param a
     * @return true if the activity was found and removed, false
     *  if the activity is not scheduled with this manager.
     */
    private boolean _removeActivity(Activity a) {
        boolean r;
        synchronized (this) {
            r = m_activities.remove(a);
            if (r) {
                if (m_activities.size() == 0) {
                    m_nextTime = Long.MAX_VALUE;
                }
            }
        }
        if (r) {
            a.setScheduled(false);
        }
        return r;
    }

    /**
     * Returns the number of scheduled activities
     * @return the number of scheduled activities
     */
    private synchronized int _activityCount() {
        return m_activities.size();
    }

    /**
     * Sets the running flag for the ActivityManager instance.
     */
    private synchronized void _setRunning(boolean b) {
        m_run = b;
    }

    /**
     * Used by the activity loop to determine if the ActivityManager
     * thread should keep running or exit.
     */
    private synchronized boolean _keepRunning() {
        return m_run;
    }

    /**
     * Main scheduling thread loop. This is automatically started upon
     * initialization of the ActivityManager.
     */
    public void run() {
        _setRunning(true);
        while (_keepRunning()) {
            if (_activityCount() > 0) {
                long currentTime = System.currentTimeMillis();
                long t = -1;

                synchronized (this) {
                    // copy content of activities, as new activities might
                    // be added while we process the current ones
                    for (int i = 0; i < m_activities.size(); i++) {
                        Activity a = (Activity) m_activities.get(i);
                        m_tmp.add(a);

                        // remove activities that won't be run again
                        if (currentTime >= a.getStopTime()) {
                            m_activities.remove(i--);
                            a.setScheduled(false);
                        }
                    }
                    // if no activities left, reflect that in the next time
                    if (m_activities.size() == 0) {
                        m_nextTime = Long.MAX_VALUE;
                    }
                }

                for (int i = 0; i < m_tmp.size(); i++) {
                    // run the activity - the activity will check for
                    // itself if it should perform any action or not
                    Activity a = (Activity) m_tmp.get(i);
                    long s = a.runActivity(currentTime);
                    // compute minimum time for next activity cycle
                    t = (s < 0 ? t : t < 0 ? s : Math.min(t, s));
                }

                // clear the temporary list
                m_tmp.clear();

                if (t == -1)
                    continue;

                // determine the next time we should run
                try {
                    synchronized (this) {
                        wait(t);
                    }
                } catch (InterruptedException e) {
                }

            } else {
                // nothing to do, chill out until notified
                try {
                    synchronized (this) {
                        wait();
                    }
                } catch (InterruptedException e) {
                }
            }
        }
    }

    public class ScheduleAfterActivity extends ActivityAdapter {
        Activity after;
        boolean remove;

        public ScheduleAfterActivity(Activity after, boolean remove) {
            this.after = after;
            this.remove = remove;
        }

        public void activityFinished(Activity a) {
            if (remove)
                a.removeActivityListener(this);
            scheduleNow(after);
        }

        public void activityCancelled(Activity a) {
            if (remove)
                a.removeActivityListener(this);
        }
    } // end of inner class ScheduleAfterActivity

} // end of class ActivityManager
